/*
 * Copyright (c) 2015-present, Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 */

package com.facebook.common.references;

import javax.annotation.concurrent.GuardedBy;

import com.facebook.common.internal.VisibleForTesting;
import com.facebook.common.internal.Preconditions;

/**
 * A shared-reference class somewhat similar to c++ shared_ptr. The underlying value is reference
 * counted, and when the count drops to zero, the underlying value is "disposed"
 * <p/>
 * Unlike the c++ implementation, which provides for a bunch of syntactic sugar with copy
 * constructors and destructors, Java does not provide the equivalents. So we instead have the
 * explicit addReference() and deleteReference() calls, and we need to be extremely careful
 * about using these in the presence of exceptions, or even otherwise.
 * <p/>
 * Despite the extra (and clunky) method calls, this is still worthwhile in many cases to avoid
 * the overhead of garbage collection.
 * <p/>
 * The somewhat clunky rules are
 * 1. If a function returns a SharedReference, it must guarantee that the reference count
 * is at least 1. In the case where a SharedReference is being constructed and returned,
 * the SharedReference constructor will already set the ref count to 1.
 * 2. If a function calls another function with a shared-reference parameter,
 * 2.1 The caller must ensure that the reference is valid for the duration of the
 * invocation.
 * 2.2 The callee *is not* responsible for the cleanup of the reference.
 * 2.3 If the callee wants to keep the reference around even after the call returns (for
 * example, stashing it away in a map), then it should "clone" the reference by invoking
 * {@link #addReference()}
 * <p/>
 * Example #1 (function with a shared reference parameter):
 * void foo(SharedReference r, ...) {
 * // first assert that the reference is valid
 * Preconditions.checkArgument(SharedReference.isValid(r));
 * ...
 * // do something with the contents of r
 * ...
 * // do not increment/decrement the ref count
 * }
 * <p/>
 * Example #2 (function with a shared reference parameter that keeps around the shared ref)
 * void foo(SharedReference r, ...) {
 * // first assert that the reference is valid
 * Preconditions.checkArgument(SharedReference.isValid(r));
 * ...
 * // increment ref count
 * r.addReference();
 * // stash away the reference
 * ...
 * return;
 * }
 * <p/>
 * Example #3 (function with a shared reference parameter that passes along the reference to
 * another function)
 * void foo(SharedReference r, ...) {
 * // first assert that the reference is valid
 * Preconditions.checkArgument(SharedReference.isValid(r));
 * ...
 * bar(r, ...); // call to other function
 * ...
 * }
 * <p/>
 * Example #4 (function that returns a shared reference)
 * SharedReference foo(...) {
 * // do something
 * ...
 * // create a new shared reference (refcount automatically at 1)
 * SharedReference r = new SharedReference(x);
 * // return this shared reference
 * return r;
 * }
 * <p/>
 * Example #5 (function with a shared reference parameter that returns the shared reference)
 * void foo(SharedReference r, ...) {
 * // first assert that the reference is valid
 * Preconditions.checkArgument(SharedReference.isValid(r));
 * ...
 * // increment ref count before returning
 * r.addReference();
 * return r;
 * }
 */
@VisibleForTesting
public class SharedReference<T> {
    private static final Class<?> TAG = SharedReference.class;

    @GuardedBy("this")
    private T mValue;
    @GuardedBy("this")
    private int mRefCount;

    private final ResourceReleaser<T> mResourceReleaser;

    /**
     * Construct a new shared-reference that will 'own' the supplied {@code value}.
     * The reference count will be set to 1. When the reference count decreases to zero
     * {@code resourceReleaser} will be used to release the {@code value}
     *
     * @param value            non-null value to manage
     * @param resourceReleaser non-null ResourceReleaser for the value
     */
    public SharedReference(T value, ResourceReleaser<T> resourceReleaser) {
        mValue = Preconditions.checkNotNull(value);
        mResourceReleaser = Preconditions.checkNotNull(resourceReleaser);
        mRefCount = 1;
    }

    /**
     * Get the current referenced value. Null if there's no value.
     *
     * @return the referenced value
     */
    public synchronized T get() {
        return mValue;
    }

    /**
     * Checks if this shared-reference is valid i.e. its reference count is greater than zero.
     *
     * @return true if shared reference is valid
     */
    public synchronized boolean isValid() {
        return mRefCount > 0;
    }

    /**
     * Checks if the shared-reference is valid i.e. its reference count is greater than zero
     *
     * @return true if the shared reference is valid
     */
    public static boolean isValid(SharedReference<?> ref) {
        return ref != null && ref.isValid();
    }

    /**
     * Bump up the reference count for the shared reference
     * Note: The reference must be valid (aka not null) at this point
     */
    public synchronized void addReference() {
        ensureValid();
        mRefCount++;
    }

    /**
     * Decrement the reference count for the shared reference. If the reference count drops to zero,
     * then dispose of the referenced value
     */
    public void deleteReference() {
        if (decreaseRefCount() == 0) {
            mResourceReleaser.release(mValue);
            synchronized (this) {
                mValue = null;
            }
        }
    }

    /**
     * Decrements reference count for the shared reference. Returns value of mRefCount after
     * decrementing
     */
    private synchronized int decreaseRefCount() {
        ensureValid();
        Preconditions.checkArgument(mRefCount > 0);

        mRefCount--;
        return mRefCount;
    }

    /**
     * Assert that there is a valid referenced value. Throw a NullReferenceException otherwise
     *
     * @throws com.facebook.common.references.SharedReference.NullReferenceException, if the reference is invalid (i.e.) the underlying value is null
     */
    private void ensureValid() {
        if (!isValid(this)) {
            throw new NullReferenceException();
        }
    }

    /**
     * A test-only method to get the ref count
     * DO NOT USE in regular code
     */
    public synchronized int getRefCountTestOnly() {
        return mRefCount;
    }

    /**
     * The moral equivalent of NullPointerException for SharedReference. Indicates that the
     * referenced object is null
     */
    public static class NullReferenceException extends RuntimeException {
        public NullReferenceException() {
            super("Null shared reference");
        }
    }
}
